﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ExcelDna.Integration;

namespace Cubicle.Excel.Shared
{
    public static class ExcelHelpers
    {

        public static string MakeRooted(string path)
        {
            return Path.IsPathRooted(path) 
                ? path 
                : Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path));
        }

        public static object[,] ToRange(this IEnumerable<string> data)
        {
            var arr = data.ToArray();
            var result = new object[arr.Length, 1];
            for (int i = 0; i < arr.Length; i++)
                result[i, 0] = arr[i];
            return result;
        }

        public static object[,] ToRange<TS, T>(
            this IEnumerable<TS> data,
            bool withHeadings,
            Func<TS, T> convert)
        {
            return ToRange(data.Select(convert).ToArray(), withHeadings);
        }

        public static object[,] ToRange<T>(this IEnumerable<T> data, bool withHeadings)
        {
            if (data == null) return null;
            var count = data.Count();
            if (count == 0) return null;
            var type = data.First().GetType();   // typeof(T) does not always work properly with dynamic types
            var properties = type.GetProperties();
            var extend = type.IsValueType ? 1 : properties.Length;
            if (extend == 0) return null;
            var offset = withHeadings ? 1 : 0;
            var length = count + offset;
            var table = new object[length, extend];

            // Add headings
            if (withHeadings)
                for (int j = 0; j < extend; j++)
                    table[0, j] = properties[j].Name;

            // Add data
            int i = 0;
            foreach (var item in data)
            {
                for (int j = 0; j < extend; j++)
                    try
                    {
                        table[i + offset, j] = type.IsValueType ? item : (object)properties[j].GetValue(item, null).ToString();
                    }
                    catch
                    {
                        table[i + offset, j] = "Null/Error";
                    }
                i++;
            }

            return table;
        }

        public static object[,] ToRange(this IEnumerable data, bool withHeadings)
        {
            if (data == null) return null;
            var enumerator = data.GetEnumerator();
            if (!enumerator.MoveNext()) return null;
            var type = enumerator.Current.GetType();   // typeof(T) does not always work properly with dynamic types
            var properties = type.GetProperties();
            var extend = type.IsValueType ? 1 : properties.Length;
            var offset = withHeadings ? 1 : 0;
            var count = 1;
            while (enumerator.MoveNext()) count++;
            var length = count + offset;
            var table = new object[length, extend];

            // Add headings
            if (withHeadings)
                for (int j = 0; j < extend; j++)
                    table[0, j] = type.IsValueType ? "Value" : properties[j].Name;

            // Add data
            int i = 0;
            foreach (var item in data)
            {
                for (int j = 0; j < extend; j++)
                    try
                    {
                        table[i + offset, j] = type.IsValueType ? item : properties[j].GetValue(item, null).ToString();
                    }
                    catch
                    {
                        table[i + offset, j] = "Null/Error";
                    }
                i++;
            }

            return table;
        }

        public static object[,] ToRange<T>(
            this object data, 
            bool withHeadings, 
            Func<object, T> convert)
        {
            var list = new [] {data};
            return list.ToRange(withHeadings, convert);
        }

        public static object[][] DoubleToJagged(object[,] p)
        {
            // Find dimensions
            var dim1 = p.GetLength(0);
            var dim2 = p.GetLength(1);

            // Check if this should be null
            if (dim1 == 1 && dim2 == 1 && p[0, 0] is ExcelMissing)
                return null;

            // Turn vertical array into horizontal
            var swap = dim2 == 1;
            var resultDim1 = swap ? 1 : dim1;
            var resultDim2 = swap ? dim1 : dim2;

            // Just get the array type
            var result = new object[resultDim1][]; // Array.CreateInstance(typeof(object), dim1);

            // Now instantiate the result array with the above type
            for (var i = 0; i < resultDim1; i++)
            {
                result[i] = new object[resultDim2];
                for (var j = 0; j < resultDim2; j++)
                    result[i][j] = swap ? Cast(p[j, i]) : Cast(p[i, j]);
            }

            return result;
        }

        public static object[] DoubleToJaggedOrArray(object data)
        {
            var p = data as object[,];

            // Find dimensions
            var dim1 = p.GetLength(0);
            var dim2 = p.GetLength(1);

            // Check if this should be null
            if (dim1 == 1 && dim2 == 1 && p[0, 0] is ExcelMissing)
                return null;

            // Turn vertical array into horizontal
            var swap = dim2 == 1;
            var resultDim1 = swap ? 1 : dim1;
            var resultDim2 = swap ? dim1 : dim2;

            // Just get the array type
            var result = new object[resultDim1][]; // Array.CreateInstance(typeof(object), dim1);

            // Now instantiate the result array with the above type
            for (var i = 0; i < resultDim1; i++)
            {
                result[i] = new object[resultDim2];
                for (var j = 0; j < resultDim2; j++)
                    result[i][j] = swap ? Cast(p[j, i]) : Cast(p[i, j]);
            }

            var returnVector = dim1 == 1 || dim2 == 1;

            return returnVector 
                ? result[0]
                : result;
        }

        public static object[][] DoubleToJagged(object data)
        {
            var p = data as Array;

            // Find dimensions
            var dim1 = p.GetLength(0);
            var dim2 = p.GetLength(1);

            // Check if this should be null
            if (dim1 == 1 && dim2 == 1 && p.GetValue(0,0) is ExcelMissing)
                return null;

            // Turn vertical array into horizontal
            var swap = dim2 == 1;
            var resultDim1 = swap ? 1 : dim1;
            var resultDim2 = swap ? dim1 : dim2;

            // Just get the array type
            var result = new object[resultDim1][]; // Array.CreateInstance(typeof(object), dim1);

            // Now instantiate the result array with the above type
            for (var i = 0; i < resultDim1; i++)
            {
                result[i] = new object[resultDim2];
                for (var j = 0; j < resultDim2; j++)
                    result[i][j] = Cast(swap ? p.GetValue(j, i) : p.GetValue(i, j));
            }

            return result;
        }

        /// <summary>
        /// Checks each element of the array. If one is found to be a double array, it is converted to a jagged.
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        public static object[] DoubleToJaggedOrArrayConvert(object[] data)
        {
            for (var i = 0; i < data.Length; i++)
            {
                data[i] = IsDouble(data[i])
                              ? DoubleToJaggedOrArray(data[i])
                              : data[i];

            }
            return data;
        }

        public static object DoubleToJagged2(object data)
        {
            var p = data as Array;

            var dim1 = p.GetLength(0);
            var dim2 = p.GetLength(1);

            // Check if this should be null
            if (dim1 == 1 && dim2 == 1 && p.GetValue(0, 0) is ExcelMissing)
                return null;

            // Turn vertical array into horizontal
            var swap = dim2 == 1;
            var resultDim1 = swap ? 1 : dim1;
            var resultDim2 = swap ? dim1 : dim2;

            // Determine the type (supported types are string[][], double[][])
            var isString = false;
            for (var i = 0; i < dim1 && !isString; i++)
                for (var j = 0; j < dim2 && !isString; j++)
                    isString = p.GetValue(i,j) is string;

            // Just get the array type
            var targetType = isString ? typeof (string) : typeof (double);
            var result = Array.CreateInstance(isString ? typeof(string[]) : typeof(double[]), resultDim1);

            // Now instantiate the result array with the above type
            for (var i = 0; i < resultDim1; i++)
            {
                var curArr = Array.CreateInstance(targetType, resultDim2);
                result.SetValue(curArr, i);
                for (var j = 0; j < resultDim2; j++)
                    if (isString)
                        curArr.SetValue(Cast(swap ? p.GetValue(j, i) : p.GetValue(i,j)).ToString(), j);
                    else
                        curArr.SetValue(CastToDouble(swap ? p.GetValue(j, i) : p.GetValue(i, j)), j);
            }

            return result;
        }

        public static double CastToDouble(object p)
        {
            return (p is ExcelMissing || p is ExcelEmpty) 
                ? 0 
                : (double)Convert.ChangeType(p, typeof(double));
        }

        public static object Cast(object p)
        {
            return (p is ExcelMissing || p is ExcelEmpty) ? null : p;
        }

        public static object Transform(object p)
        {
            if (p is string) return p;
            if (IsSingleArray(p)) return ArrayToDouble(p);
            if (IsJagged(p)) return JaggedToDouble(p);
            if (IsEnumerable(p)) return EnumerableToDouble(p);
            return p;
        }

        private static object EnumerableToDouble(object o)
        {
            var enumerable = (IEnumerable)o;
            return enumerable.Cast<object>().ToArray();
        }

        private static bool IsEnumerable(object o)
        {
            return o is IEnumerable;
        }

        public static bool IsJagged(object p)
        {
            var type = p.GetType();
            if (!type.IsArray) return false;
            if (type.GetArrayRank() != 1) return false;
            var arr = (Array) p;
            var length = arr.GetLength(0);
            if (length == 0) return false;
            return arr.GetValue(0).GetType().IsArray;
        }

        public static bool IsSingleArray(object p)
        {
            var type = p.GetType();
            return (type.IsArray && type.HasElementType && !type.GetElementType().IsArray);
        }

        public static object ArrayToDouble(object p)
        {
            if (!IsSingleArray(p)) return p;

            var arr = (Array)p;
            var result = new object[arr.Length];
            for (var i = 0; i < arr.Length; i++)
                result[i] = arr.GetValue(i);
            return result;
        }

        public static object JaggedToDouble(object p)
        {
            if (!IsJagged(p)) return p;

            var oldResult = p as Array;
            var dim1 = oldResult.Length;
            
            // Assume the jagged array is square
            var dim2 = Enumerable.Range(0, dim1).Min(i => ((Array) oldResult.GetValue(i)).Length);
            
            // Init double array
            var result = new object[dim1,dim2];

            // Populate double array
            Array temp;
            for (var i = 0; i < dim1; i++ )
            {
                temp = (Array) oldResult.GetValue(i);
                for (var j = 0; j < dim2; j++)
                    result[i, j] = temp.GetValue(j);
            }

            return result;
        }

        public static bool IsDouble(object o)
        {
            var type = o.GetType();
            return type.IsArray && type.GetArrayRank() == 2;
        }
    }
}
